// Downloader
package Downloader

import (
	"bufio"
	"bytes"
	"io"
	"log"
	"net"
	"os"
	"regexp"
	"strconv"
	"strings"
	"time"
)

type Context struct {
	host     string
	ip       string
	port     string
	path     string
	protocal string
	fileName string
}

//type FPInt int //File Position Int

type Downloader struct {
	fd         *os.File
	ctx        *Context
	speed      *Speed
	timeout    time.Duration
	readChunck int
	fileLength int64
}

type RoutineContext struct {
	startPos int64
	endPos   int64
	success  bool
	data     []byte
	nByte    int
	id       int
	speed    *Speed
	progress *Progress
}

func NewDownloader() *Downloader {
	var ret Downloader
	ret.readChunck = 1024 * 8
	ret.ctx = new(Context)
	ret.timeout = time.Second * 10
	ret.speed = NewSpeed()
	return &ret
}

func (this *Downloader) extractContentLength(body string) int64 {
	reg := regexp.MustCompile("(?i)content-length\\s*:\\s*(\\d+)")
	allString := reg.FindAllStringSubmatch(body, -1)
	//for _, v := range allString {
	//	log.Println(v)
	//}
	length, err := strconv.ParseInt(allString[0][1], 10, 32)
	if err != nil {
		log.Println("parse content length:", err)
	}
	return length
}

func (this *Downloader) Newfd(allocatedSize int64) *os.File {
	dirPath := "./"
	file, err := os.Create(dirPath + this.ctx.fileName)
	if err != nil {
		log.Println("create file:", err)
		return nil
	}
	chunck := int64(1024 * 1024 * 4)
	empty := make([]byte, chunck)
	var writenSize int64
	writenSize = 0
	for writenSize < allocatedSize {
		var n int
		n = 0
		if allocatedSize-writenSize < chunck {
			n, err = file.Write(empty[:allocatedSize-writenSize])
		} else {
			n, err = file.Write(empty)
		}

		if err != nil {
			log.Println("allocating file:", err)
			return nil
			break
		}
		writenSize += int64(n)

	}
	this.fd = file
	file.Seek(0, 0)
	return file
}

func (this *Downloader) goRoutine(jobCtx *RoutineContext, ctxCh chan *RoutineContext) {
	ctx := jobCtx
	log.Println("routine", ctx.id, "started")
	startPos := ctx.startPos
	endPos := ctx.endPos
	requestBody := this.makeFileRequestHeaderWithRange(startPos, endPos)
	log.Println("request from", startPos, "to", endPos, "id", ctx.id)
	if dataToBeWriten, nBytes, ok := this.receiveFile(requestBody); !ok {
		//由于读取失败，所以重新来一次
		//
		log.Println("read from remote failed from", startPos, "to", endPos, "id", ctx.id)
		ctx.success = false
	} else {
		ctx.success = true
		ctx.data = dataToBeWriten
		ctx.nByte = nBytes
		jobCtx.speed.Increase(nBytes)
		jobCtx.progress.Increase(nBytes)

	}
	log.Println("ready to return id", ctx.id)
	ctxCh <- ctx
}

func (this *Downloader) hostToIp(host string) string {
	ips, err := net.LookupIP(host)
	if err != nil {
		log.Println("hostToIp: ", err.Error())
	}
	return ips[0].String()
}

func (this *Downloader) makeFileRequestHeader() []byte {
	ctx := this.ctx
	host := ctx.host
	port := ctx.port
	path := ctx.path
	endOfLine := "\n"
	strBody := ""
	strBody += "GET " + path + " HTTP/1.1" + endOfLine
	strBody += "Host: " + host + ":" + port + endOfLine
	strBody += "Accept: */*" + endOfLine
	strBody += "User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36" + endOfLine
	strBody += "Connection: close" + endOfLine
	strBody += endOfLine
	buf := bytes.NewBufferString(strBody)
	return buf.Bytes()
}

func (this *Downloader) makeFileRequestHeaderWithRange(from int64, to int64) []byte {
	ctx := this.ctx
	host := ctx.host
	port := ctx.port
	path := ctx.path
	endOfLine := "\n"
	strBody := ""
	strBody += "GET " + path + " HTTP/1.1" + endOfLine
	strBody += "Host: " + host + ":" + port + endOfLine
	strBody += "Accept: */*" + endOfLine
	strBody += "User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36" + endOfLine
	strBody += "Connection: close" + endOfLine
	strBody += "Range: bytes=" + strconv.FormatInt(from, 10) + "-" + strconv.FormatInt(to, 10) + endOfLine
	strBody += endOfLine
	buf := bytes.NewBufferString(strBody)
	return buf.Bytes()
}

func (this *Downloader) protocolHostPort(url string) (ctx *Context, valid bool) {
	//先把协议取出来
	reg := regexp.MustCompile("([a-z]+)://(.*)")
	allString := reg.FindAllStringSubmatch(url, -1)
	//for _, v := range allString {
	//	log.Println(v)
	//}
	protocal := allString[0][1]
	remains := allString[0][2]
	defaultPort := true
	if strings.Contains(remains, ":") {
		reg = regexp.MustCompile("(.*?)(:?\\d*)(/.*)")
		defaultPort = false
	} else {
		reg = regexp.MustCompile("(.*?)(/.*)")
	}
	allString = reg.FindAllStringSubmatch(remains, -1)
	//for _, v := range allString {
	//	log.Println(v)
	//}
	var host string
	var port string
	var path string
	if defaultPort {
		host = allString[0][1]
		port = "80"
		path = allString[0][2]
	} else {
		host = allString[0][1]
		port = allString[0][2][1:]
		path = allString[0][3]
	}
	//取文件名
	subPath := strings.Split(path, "/")
	var fileName string
	if len(subPath) > 0 {
		fileName = strings.Trim(subPath[len(subPath)-1], " \r")
	}
	if fileName == "" {
		fileName = "unknown"
	}
	//做一些基本的检查
	ctx = new(Context)
	ctx.protocal = protocal
	ctx.host = host
	ctx.port = port
	ctx.path = path
	ctx.fileName = fileName
	valid = this.validUrlCheck(ctx)
	return
}

func (this *Downloader) request(body []byte) (retResp []byte, n int) {
	ipPort := this.ctx.host + ":" + this.ctx.port
	//log.Println("send body\n", string(body))
	conn, err := net.Dial("tcp", ipPort)
	defer conn.Close()
	if err != nil {
		log.Println("request connection error: ", err.Error())
	}
	_, err = conn.Write(body)
	if err != nil {
		log.Println("send data error: ", err.Error())
	}
	reader := bufio.NewReader(conn)
	ch := make(chan bool)
	n = 0
	go func(ch chan bool) {
		for true {
			line, err := reader.ReadString('\n')
			if err != nil {
				if err == io.EOF {
					break
				}
				log.Println("read header")
				log.Println(err)
				ch <- false
				return
			}
			lineBytes := []byte(line)
			n += len(lineBytes)
			retResp = append(retResp, lineBytes[:]...)
			newLine := strings.Trim(line, " \r\n")
			if newLine == "" {
				ch <- true
				return
			}
		}
	}(ch)
	select {
	case <-time.After(this.timeout):
		log.Println("request header timeout")
	case <-ch:
	}

	return

}

func (this *Downloader) receiveFile(body []byte) (dataToBeWriten []byte, nBytes int, ret bool) {
	ipPort := this.ctx.host + ":" + this.ctx.port
	conn, err := net.Dial("tcp", ipPort)
	if err != nil {
		log.Println("request connection error: ", err.Error())
		ret = false
		return
	}
	defer conn.Close()
	ch := make(chan bool)
	length := int64(1024)
	go func(ipPort string, body []byte, ch chan bool /*, tm string*/) {
		_, err = conn.Write(body)
		if err != nil {
			log.Println("send data error: ", err.Error())
		}
		//读头信息
		reader := bufio.NewReader(conn)
		for true {
			line, err := reader.ReadString('\n')
			if err != nil {
				if err == io.EOF {
					break
				}
				log.Println("read header")
				log.Println(err)
				ch <- false
				return
			}
			newLine := strings.Trim(line, " \r\n")
			if newLine == "" {
				break
			}

			if strings.Contains(strings.ToLower(newLine), "content-length") {
				length = this.extractContentLength(newLine)
			}
		}
		//写文件
		var n int
		var totalRead int
		dataToBeWriten = make([]byte, 0, this.readChunck)
		totalRead = 0
		resp := make([]byte, length)
		for true {
			n, err = conn.Read(resp)
			if err != nil {
				if io.EOF == err {
					if totalRead < int(length) {
						log.Println("not enough bytes writen ", totalRead)
						ch <- false
						return
					}
					ch <- true
					return
				} else {
					log.Println("read respond err", err)
					ch <- false
					return
				}

			} else {
				totalRead += n
				nBytes = totalRead
				dataToBeWriten = append(dataToBeWriten, resp[:n]...)

			}

		}
	}(ipPort, body, ch /*, tm*/)
	select {
	case <-time.After(this.timeout):
		log.Println("receive timeout")
		ret = false
	case ret = <-ch:

	}
	return
}

func (this *Downloader) validUrlCheck(ctx *Context) bool {
	protocal := ctx.protocal
	if protocal != "http" {
		return false
	}
	//host := ctx.host
	port := ctx.port
	if numPort, err := strconv.Atoi(port); err != nil {
		return false
	} else {
		if numPort < 0 {
			return false
		}
	}
	//path := ctx.path
	return true
}

func (this *Downloader) Get(url string) {
	defer this.fd.Close()
	if ctx, ok := this.protocolHostPort(url); !ok {
		log.Fatalln("url invalid.")
	} else {
		this.ctx = ctx
	}

	log.Println(this.ctx.path)
	requestBody := this.makeFileRequestHeader()
	resp, n := this.request(requestBody)
	strResp := string(resp[:n])
	length := this.extractContentLength(strResp)
	progress := NewProgress(length)
	progress.AutomaticallyShow()
	log.Println(length)
	this.Newfd(length)
	//确定分成多少份。
	smallestBlock := this.readChunck
	segment := int(length/int64(smallestBlock) + 1)
	initSegment := 5
	if segment-1 < initSegment {
		initSegment = segment - 1
	}
	var startOffset int64
	startOffset = 0
	routineCtxCh := make(chan *RoutineContext, 2)
	var jobCtx *RoutineContext
	//初始开始的routine
	this.speed.AutomaticallyShow()
	for i := 0; i < initSegment; i++ {
		var toOffset int64
		toOffset = startOffset + int64(smallestBlock) - 1
		jobCtx = new(RoutineContext)
		jobCtx.startPos = startOffset
		jobCtx.endPos = toOffset
		jobCtx.id = i
		jobCtx.speed = this.speed
		jobCtx.progress = progress
		startOffset += int64(smallestBlock)
		go this.goRoutine(jobCtx, routineCtxCh)
		log.Println("start routine", i)
	}
	nextSegment := initSegment
	for i := 0; i < initSegment; i++ {
		log.Println("waiting")
		routineCtx := <-routineCtxCh
		log.Println("current routine", initSegment-i)
		log.Println("routine", routineCtx.id, "return")
		if !routineCtx.success {
			log.Print("routine failed id", routineCtx.id)
			jobCtx = routineCtx
		} else {
			dataToBeWriten := routineCtx.data
			nBytes := routineCtx.nByte
			this.fd.Seek(int64(routineCtx.startPos), 0)
			if n, err := this.fd.Write(dataToBeWriten[:nBytes]); err != nil {
				log.Fatalln("writen to file", err)
			} else {
				log.Println("writen", n, "bytes", "from", routineCtx.startPos, "to", routineCtx.endPos, "id", routineCtx.id)
			}
			nextSegment++
			if nextSegment > segment {
				continue //不分配新的任务，不执行新的routine
			}
			var toOffset int64
			toOffset = 0
			if nextSegment == segment {
				toOffset = length - 1
			} else {
				toOffset = startOffset + int64(smallestBlock) - 1
			}
			jobCtx = new(RoutineContext)
			jobCtx.startPos = startOffset
			jobCtx.endPos = toOffset
			jobCtx.id = routineCtx.id
			jobCtx.speed = this.speed
			jobCtx.progress = progress
			startOffset += int64(smallestBlock)
		}
		i--
		log.Println("starting routine id", jobCtx.id)
		go this.goRoutine(jobCtx, routineCtxCh) //不会得到执行，如果nextSegment > segment

	}
	progress.Show()

}
